package distributed_lock

import (
	"context"
	"errors"
	"github.com/go-redis/redis/v8"
	"time"
)

const (
	// RedisLockKeyPrefix redis锁的前缀
	RedisLockKeyPrefix = "REDIS_LOCK_PREFIX_"
	// RedisLockDefaultValue redis锁默认值
	RedisLockDefaultValue = "1"
)

type RedisLock struct {
	client *redis.Client
}

func NewRedisLock(client *redis.Client) *RedisLock {
	return &RedisLock{client: client}
}

func (r *RedisLock) TryLock(ctx context.Context, key string, expireTime, waitTime time.Duration) (bool, error) {
	if waitTime <= 0 {
		waitTime = DefaultRetryDelayTimes
	}
	for i := 0; i < int(waitTime/DefaultRetryDelayTimes); i++ {
		locked, err := r.lock(ctx, key, expireTime)
		if locked || err != nil {
			return locked, err
		}
		time.Sleep(DefaultRetryDelayTimes)
	}
	return false, nil
}

func (r *RedisLock) UnLock(ctx context.Context, key string) (bool, error) {
	script := redis.NewScript(`
	if redis.call("get", KEYS[1]) == ARGV[1] then
		return redis.call("del", KEYS[1])
	else
		return 0
	end
	`)
	if ctx == nil {
		ctx = context.Background()
	}
	redisKey := RedisLockKeyPrefix + key
	result, err := script.Run(ctx, r.client, []string{redisKey}, RedisLockDefaultValue).Int64()
	if err != nil {
		return false, err
	}

	return result > 0, nil
}

func (r *RedisLock) RefreshLock(ctx context.Context, key string, expireTime time.Duration) (bool, error) {
	script := redis.NewScript(`
	local val = redis.call("GET", KEYS[1])
	if not val then
		redis.call("setex", KEYS[1], ARGV[2], ARGV[1])
		return 2
	elseif val == ARGV[1] then
		return redis.call("expire", KEYS[1], ARGV[2])
	else
		return 0
	end
	`)
	if ctx == nil {
		ctx = context.Background()
	}
	redisKey := RedisLockKeyPrefix + key
	result, err := script.Run(context.Background(), r.client, []string{redisKey}, RedisLockDefaultValue, expireTime/time.Second).Int64()
	if err != nil {
		return false, err
	}

	return result > 0, nil

}

func (r *RedisLock) lock(ctx context.Context, key string, expireTime time.Duration) (bool, error) {
	if expireTime < 0 {
		expireTime = DefaultExpiration
	}
	if r.client == nil {
		return false, errors.New("redis client is nil")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	redisKey := RedisLockKeyPrefix + key
	result, err := r.client.SetNX(ctx, redisKey, RedisLockDefaultValue, expireTime).Result()
	if err != nil {
		return false, err
	}
	return result, nil
}

var _ DistributedLock = &RedisLock{}
